package dht.threads;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.math.BigInteger;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.zip.GZIPInputStream;

import dht.communication.CommunicationDHT;
import dht.communication.FabriqueMessage;
import dht.communication.PortThread;
import dht.data.Contexte;
import dht.data.DhtData;
import dht.data.TabBigInteger;
import dht.interfaceThread.InterQuit;
import dht.message.AckMessage;
import dht.message.Message;
import dht.message.MissMessage;
import dht.message.QuitMessage;
import dht.message.exception.ErrorMessage;
import dht.node.Node;
import dht.tools.Couple;

/**
 * Thread qui gère la reception et l'envoie de message QUIT.
 * @author bouceka
 *
 */
public class ManageQuit extends Thread implements InterQuit {

	private ArrayList<ArrayList<Couple<String>>> voisins = null;
	private DatagramSocket maSocket = null;
	private DatagramSocket socketReceiveQUIT = null;
	private DatagramSocket socketReceiveACK = null;
	private ServerSocket socketReceive_QUIT_CTX = null;
	private Message msgRecu = null;

	public ManageQuit(boolean initNode){
		try {
			this.maSocket = new DatagramSocket();
			this.socketReceiveQUIT = new DatagramSocket(PortThread.PORT_QUIT);
			this.socketReceiveACK = new DatagramSocket(PortThread.PORT_QUIT_ACK);
			this.socketReceive_QUIT_CTX = new ServerSocket(PortThread.PORT_RECEIVE_QUIT_CTX);
		} catch (SocketException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

	}

	/**
	 * Test si deux zones se chevauchent
	 * @param z1 1ere zone
	 * @param z2 2eme zone
	 * @return vrai si elles se chevauvent, faux sinon
	 */

	public boolean overlap(Couple<BigInteger> z1, Couple<BigInteger> z2){

		if(z1.getElem2().compareTo(z2.getElem2()) < 0){
			if(z2.getElem1().min(z1.getElem2()) != BigInteger.ONE){
				return false;
			}
		}
		else if(z1.getElem2().compareTo(z2.getElem2()) > 0){
			if(z1.getElem1().min(z2.getElem2()) != BigInteger.ONE){
				return false;
			}
		}
		else{
			// Ne devrait JAMAIS se produire !
			System.out.println("Probleme dans la DHT complete!");
			System.exit(-1);
		}

		return true;
	}

	/**
	 * Verifie que la zone peut etre fusionne avec la zone locale du noeud
	 * @param zone
	 * @return le numero de la dimension qui differe si c'est fusionable, -1 sinon.
	 */
	private int isMerge(ArrayList<Couple<BigInteger>> zone){

		int cpt = 0, res = 0;

		synchronized(Node.myContext){
			for(int i=0 ; i<Node.NB_DIMENSION; i++){
				Couple<BigInteger> c_zone = zone.get(i);
				Couple<BigInteger> c_zoneLocale = Node.myContext.getZoneLocale().get(i);

				if( (c_zone.getElem1().compareTo(c_zoneLocale.getElem1()) != 0 ) 
						|| (c_zone.getElem2().compareTo(c_zoneLocale.getElem2()) != 0 )){

					if(overlap(c_zone,c_zoneLocale)){ // Si chevauchement la fusion n'est pas possible
						return -1;
					}
					cpt++;
					res = i;
				}

				/*
				 * Si plus de une dimension differente alors la zone ne peut
				 * etre mergé.
				 */
				if(cpt  > 1){
					return -1;
				}
			}
		}
		return res;
	}

	/**
	 * Fusionne deux zones qui sont fusionnable
	 * @param ctx Le contexte contenant la zone et la dht a fusionne.
	 * @param dim La dimension qui differe entre les deux zones.
	 */
	private void fusionnerZone(Contexte ctx, int dim){

		ArrayList<Couple<BigInteger>> zone = ctx.getZoneLocale();
		HashMap<TabBigInteger, DhtData<Integer>> dht = ctx.getDhtLocale();
		Couple<BigInteger> newDim = new Couple<BigInteger>(null,null);

		synchronized(Node.myContext){

			if(zone.get(dim).getElem2().compareTo(Node.myContext.getZoneLocale().get(dim).getElem2()) > 0){
				newDim.setElem2(zone.get(dim).getElem2());
				newDim.setElem1(Node.myContext.getZoneLocale().get(dim).getElem1());
			}
			else{
				newDim.setElem2(Node.myContext.getZoneLocale().get(dim).getElem2());
				newDim.setElem1(zone.get(dim).getElem1());
			}

			// Mise a jour de la zone dans la dimension dim
			Node.myContext.getZoneLocale().set(dim, newDim);

			// On ajoute tous les eleme de la dht recu dans la dht locale
			for(TabBigInteger elem : dht.keySet()){
				Node.myContext.getDhtLocale().put(elem, dht.get(elem));
			}
		}
		return;
	}
	
	private void majVoisins(String addrr){
		
		synchronized(Node.myContext){
			
			for(ArrayList<Couple<String>> liste : Node.myContext.getVoisinage()){
				for(Couple<String> elem : liste){
					if(elem.getElem2().compareTo(addrr) == 0){
						liste.remove(elem);
						break;
					}
				}
			}
		}
		
		return;
	}

	//Receptionne le contexte puis soit le fusionne, soit le rajoute a la liste des contextes non fusionné
	private void receiveContext() throws ErrorMessage {

		Socket sc = null;
		ObjectInputStream ois = null;
		Contexte ctx = null;

		try {
			sc = this.socketReceive_QUIT_CTX.accept();
		} catch (IOException e) {
			e.printStackTrace();
		}

		try {
			ois = new ObjectInputStream(new GZIPInputStream(sc.getInputStream()));
			ctx = (Contexte)ois.readObject();
			ois.close();

		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		int laDim = this.isMerge(ctx.getZoneLocale());

		// On peut le merger 
		if(laDim != -1){
			this.fusionnerZone(ctx,laDim);
			this.majVoisins(ctx.getAdresseIP().toString().substring(1));
		}
		else{ // On l'ajoute aux contextes non fusionne
			synchronized(Node.contexteNonFusion){
				Node.contexteNonFusion.add(ctx);
			}
		}

		System.out.println("J'ai recu un contexte ! \n" + Node.myContext);
		return;
	}


	public void receiveRequestQuit() throws ErrorMessage{

		DatagramPacket in = null,dp_AckMsg = null;
		byte[] buff = null;
		ByteArrayInputStream monStream = null;
		ObjectInputStream ois = null;

		buff = new byte[65536];
		in  = new DatagramPacket(buff,65536);

		System.out.println("Thread ManageQuit : Attente requete QUIT...");

		try {
			this.socketReceiveQUIT.receive(in); // Attente d'une requete
		} catch (IOException e) {
			e.printStackTrace();
		}

		monStream = new ByteArrayInputStream(buff);

		try {
			ois = new ObjectInputStream(monStream);
			this.msgRecu = (Message)ois.readObject();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}

		if(this.msgRecu.TYPE_MSG == Message.QUIT_Message){

			System.out.println("J'ai recu requete QUIT");
			// On envoie un message ACK 
			AckMessage aMsg = FabriqueMessage.createAckMessage(Node.monAdresse);
			dp_AckMsg = CommunicationDHT.packetForgeToKnownNode(aMsg, this.msgRecu.getadresseIpExp(), PortThread.PORT_QUIT_ACK);
			CommunicationDHT.sendMessage(dp_AckMsg, this.maSocket);

			try {
				this.socketReceiveACK.receive(in); // Attente du MISS ou du ACK
			} catch (IOException e) {
				e.printStackTrace();
			}

			System.out.println("c bon jai atendu miss ou ack");
			monStream = new ByteArrayInputStream(buff);

			try {
				ois = new ObjectInputStream(monStream);
				this.msgRecu = (Message)ois.readObject();
			} catch (IOException e) {
				e.printStackTrace();
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			}

			if(this.msgRecu.TYPE_MSG == Message.ACK_Message){

				System.out.println("Je vais recevoir le contexte");
				// Reception du contexte et traitement
				try {
					this.receiveContext();
				} catch (ErrorMessage e) {
					e.printStackTrace();
				}
			}
			else if(this.msgRecu.TYPE_MSG == Message.MISS_Message){

				InetAddress takeOverNode = ((MissMessage)this.msgRecu).takeOverNode;
				String takeOverVID = ((MissMessage)this.msgRecu).takeOverNodeVID;

				synchronized(Node.myContext){

					voisins = Node.myContext.getVoisinage();

					for(int i=0; i<voisins.size(); i++){
						for(int j=0; i<voisins.get(i).size(); j++){
							if(voisins.get(i).get(j).getElem2().compareTo(this.msgRecu.getadresseIpExp().toString()) == 0){
								voisins.get(i).get(j).setElem1(takeOverVID);
								voisins.get(i).get(j).setElem2(takeOverNode.getHostAddress());
								break;
							}
						}
					}
				}
			}
			else{
				throw new ErrorMessage();
			}
		}
		else{
			throw new ErrorMessage();
		}

		return;
	}

	/**
	 * Fonction qui permet de quitter la DHT
	 */
	public void quitter_dht(){

		InetAddress source = null;
		QuitMessage qMsg = null;

		try {
			source = InetAddress.getByName(CommunicationDHT.getLocalIp());
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}

		qMsg = FabriqueMessage.createQuitMessage(source);

		try {
			this.sendRequestQuit(qMsg);
		} catch (ErrorMessage e) {
			e.printStackTrace();
		}

		return;
	}

	public boolean sendRequestQuit(QuitMessage qMsg) throws ErrorMessage {


		InetAddress source = null, dest = null;
		DatagramPacket dp_QuitMsg = null, dp_MissMsg = null, dp_AckMsg = null, in = null;
		byte[] buff = new byte[65536];
		MissMessage mMsg = null;
		AckMessage aMsg = null;
		ByteArrayInputStream monStream = null;
		ObjectInputStream ois = null;

		try {
			source = InetAddress.getByName(CommunicationDHT.getLocalIp());
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}

		synchronized(Node.myContext){

			voisins = Node.myContext.getVoisinage();

			//Envoie requete QUIT a tous les voisins
			for(int i=0; i<voisins.size(); i++){
				for(int j=0; j<voisins.get(i).size(); j++){

					try{
						dest = InetAddress.getByName(voisins.get(i).get(j).getElem2()); // elem.getElem2() = adresse IP du voisin
					} catch (UnknownHostException e) {
						e.printStackTrace();
					}

					dp_QuitMsg = CommunicationDHT.packetForgeToKnownNode(qMsg, dest, PortThread.PORT_QUIT);
					CommunicationDHT.sendMessage(dp_QuitMsg, this.maSocket);
				}
			}
		}

		// Attente du premier qui renverra un ACK
		in  = new DatagramPacket(buff,65536);

		try {
			this.socketReceiveACK.receive(in);
		} catch (IOException e) {
			e.printStackTrace();
		}

		monStream = new ByteArrayInputStream(buff);

		try {
			ois = new ObjectInputStream(monStream);
			msgRecu = (Message)ois.readObject();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}

		if(this.msgRecu.TYPE_MSG != Message.ACK_Message){
			throw new ErrorMessage();
		}

		// L'adresse du noeud qui devra gerer la zone courante et son vid
		InetAddress takeOverNode = in.getAddress();
		String takeOverNodeVID = this.msgRecu.getVirtualId();

		// On lui envoie le ACK
		aMsg = FabriqueMessage.createAckMessage(source);
		dp_AckMsg = CommunicationDHT.packetForgeToKnownNode(aMsg, takeOverNode, PortThread.PORT_QUIT_ACK);
		CommunicationDHT.sendMessage(dp_AckMsg, this.maSocket);

		mMsg = FabriqueMessage.createMissMessage(source, takeOverNode,takeOverNodeVID);

		synchronized(Node.myContext){

			voisins = Node.myContext.getVoisinage();

			// Envoie MISS contenant l'adresse du takeOverNode aux autres voisins.
			for(int i=0; i<voisins.size(); i++){
				for(int j=0; j<voisins.get(i).size(); j++){
					if(takeOverNode.getHostAddress().compareTo(voisins.get(i).get(j).getElem2())  != 0){

						try {
							dest = InetAddress.getByName(voisins.get(i).get(j).getElem2()); // elem.getElem2() = adresse IP du voisin
						} catch (UnknownHostException e) {
							e.printStackTrace();
						}

						dp_MissMsg = CommunicationDHT.packetForgeToKnownNode(mMsg, dest, PortThread.PORT_QUIT_ACK);
						CommunicationDHT.sendMessage(dp_MissMsg, this.maSocket);
					}
				}
			}
		}

		// Envoie du contexte courant au takeOverNode
		if(CommunicationDHT.sendContexte(null,takeOverNode,PortThread.PORT_RECEIVE_QUIT_CTX) < 0){
			return false;
		}

		System.out.println("Le contexte a ete envoye au noeud takeOverNode :  " + takeOverNode.getHostAddress());
		System.out.println("Je peux donc quitter le reseau, see ya !");

		return true;
	}

	public void run() {

		while(true){
			try {
				this.receiveRequestQuit();
			} catch (ErrorMessage e) {
				e.printStackTrace();
			}
		}
	}
}
